Passed
Pull Request — master (#127)
by
unknown
01:38
created

index.js ➔ readSync   F

Complexity

Conditions 21

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 6
rs 0
c 0
b 0
f 0
cc 21

How to fix   Complexity   

Complexity

Complex classes like index.js ➔ readSync often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
const fs = require('fs')
2
const ID3Definitions = require("./src/ID3Definitions")
3
const ID3Frames = require('./src/ID3Frames')
4
const ID3Util = require('./src/ID3Util')
5
const zlib = require('zlib')
6
const { isFunction, isString } = require('./src/util')
7
8
/*
9
**  Used specification: http://id3.org/id3v2.3.0
10
*/
11
12
function writeInBuffer(tags, buffer, fn) {
13
    buffer = removeTagsFromBuffer(buffer) || buffer
14
    const completeBuffer = Buffer.concat([tags, buffer])
15
    if(isFunction(fn)) {
16
        fn(null, completeBuffer)
17
        return undefined
18
    }
19
    return completeBuffer
20
}
21
22
function writeAsync(tags, filename, fn) {
23
    try {
24
        fs.readFile(filename, function(error, data) {
25
            if(error) {
26
                fn(error)
27
                return
28
            }
29
            data = removeTagsFromBuffer(data) || data
30
            const newData = Buffer.concat([tags, data])
31
            fs.writeFile(filename, newData, 'binary', (err) => {
32
                fn(err)
33
            })
34
        }.bind(this))
0 ignored issues
show
unused-code introduced by
The call to bind does not seem necessary since the function does not use this. Consider calling it directly.
Loading history...
35
    } catch(err) {
36
        fn(err)
37
    }
38
}
39
40
function writeSync(tags, filename) {
41
    try {
42
        let data = fs.readFileSync(filename)
43
        data = removeTagsFromBuffer(data) || data
44
        const newData = Buffer.concat([tags, data])
45
        fs.writeFileSync(filename, newData, 'binary')
46
    } catch(error) {
47
        return error
48
    }
49
    return true
50
}
51
52
/**
53
 * Write passed tags to a file/buffer
54
 * @param tags - Object containing tags to be written
55
 * @param filebuffer - Can contain a filepath string or buffer
56
 * @param fn - (optional) Function for async version
57
 * @returns {boolean|Buffer|Error}
58
 */
59
module.exports.write = function(tags, filebuffer, fn) {
60
    const completeTags = this.create(tags)
61
62
    if(filebuffer instanceof Buffer) {
63
        return writeInBuffer(completeTags, filebuffer, fn)
64
    }
65
    if(isFunction(fn)) {
66
        return writeAsync(completeTags, filebuffer, fn)
67
    }
68
    return writeSync(completeTags, filebuffer)
69
}
70
71
/**
72
 * Creates a buffer containing the ID3 Tag
73
 * @param tags - Object containing tags to be written
74
 * @param fn fn - (optional) Function for async version
75
 * @returns {Buffer}
76
 */
77
module.exports.create = function(tags, fn) {
78
    const frames = this.createBuffersFromTags(tags)
79
80
    //  Calculate ID3 body frames size for the header
81
    const framesSize = frames.reduce((size, frame) => size + frame.length, 0)
82
83
    //  Create ID3 header
84
    const header = Buffer.alloc(10)
85
    header.fill(0)
86
    header.write("ID3", 0)              //File identifier
87
    header.writeUInt16BE(0x0300, 3)     //Version 2.3.0  --  03 00
88
    header.writeUInt16BE(0x0000, 5)     //Flags 00
89
    ID3Util.encodeSize(framesSize).copy(header, 6)
90
91
    const id3Data = [header].concat(frames)
92
93
    if(isFunction(fn)) {
94
        fn(Buffer.concat(id3Data))
95
        return undefined
96
    }
97
    return Buffer.concat(id3Data)
98
}
99
100
/**
101
 * Returns array of buffers created by tags specified in the tags argument
102
 * @param tags - Object containing tags to be written
103
 * @returns {Array}
104
 */
105
module.exports.createBuffersFromTags = function(tags) {
106
    let frames = []
107
    if(!tags) {
108
        return frames
109
    }
110
    const rawObject = Object.keys(tags).reduce((acc, val) => {
111
        if(ID3Definitions.FRAME_IDENTIFIERS.v3[val] !== undefined) {
112
            acc[ID3Definitions.FRAME_IDENTIFIERS.v3[val]] = tags[val]
113
        } else if(ID3Definitions.FRAME_IDENTIFIERS.v4[val] !== undefined) {
114
            /**
115
             * Currently, node-id3 always writes ID3 version 3.
116
             * However, version 3 and 4 are very similar, and node-id3 can also read version 4 frames.
117
             * Until version 4 is fully supported, as a workaround, allow writing version 4 frames into a version 3 tag.
118
             * If a reader does not support a v4 frame, it's (per spec) supposed to skip it, so it should not be a problem.
119
             */
120
            acc[ID3Definitions.FRAME_IDENTIFIERS.v4[val]] = tags[val]
121
        } else {
122
            acc[val] = tags[val]
123
        }
124
        return acc
125
    }, {})
126
127
    Object.keys(rawObject).forEach((specName) => {
128
        let frame
129
        // Check if invalid specName
130
        if(specName.length !== 4) {
131
            return
132
        }
133
        if(ID3Frames[specName] !== undefined) {
134
            frame = ID3Frames[specName].create(rawObject[specName], 3, this)
135
        } else if(specName.startsWith('T')) {
136
            frame = ID3Frames.GENERIC_TEXT.create(specName, rawObject[specName], 3)
137
        } else if(specName.startsWith('W')) {
138
            if(ID3Util.getSpecOptions(specName, 3).multiple && rawObject[specName] instanceof Array && rawObject[specName].length > 0) {
139
                frame = Buffer.alloc(0)
140
                // deduplicate array
141
                for(let url of [...new Set(rawObject[specName])]) {
142
                    frame = Buffer.concat([frame, ID3Frames.GENERIC_URL.create(specName, url, 3)])
143
                }
144
            } else {
145
                frame = ID3Frames.GENERIC_URL.create(specName, rawObject[specName], 3)
146
            }
147
        }
148
149
        if (frame && frame instanceof Buffer) {
150
            frames.push(frame)
151
        }
152
    })
153
154
    return frames
155
}
156
157
function readSync(filebuffer, options) {
158
    if(isString(filebuffer)) {
159
        filebuffer = fs.readFileSync(filebuffer)
160
    }
161
    return this.getTagsFromBuffer(filebuffer, options)
162
}
163
164
function readAsync(filebuffer, options, fn) {
165
    if(isString(filebuffer)) {
166
        fs.readFile(filebuffer, (error, data) => {
167
            if(error) {
168
                fn(error, null)
169
            } else {
170
                fn(null, this.getTagsFromBuffer(data, options))
171
            }
172
        })
173
    } else {
174
        fn(null, this.getTagsFromBuffer(filebuffer, options))
175
    }
176
}
177
178
/**
179
 * Read ID3-Tags from passed buffer/filepath
180
 * @param filebuffer - Can contain a filepath string or buffer
181
 * @param options - (optional) Object containing options
182
 * @param fn - (optional) Function for async version
183
 * @returns {boolean}
184
 */
185
module.exports.read = function(filebuffer, options, fn) {
186
    if(!options || typeof options === 'function') {
187
        fn = fn || options
188
        options = {}
189
    }
190
    if(isFunction(fn)) {
191
        return readAsync.bind(this)(filebuffer, options, fn)
192
    }
193
    return readSync.bind(this)(filebuffer, options)
194
}
195
196
/**
197
 * Update ID3-Tags from passed buffer/filepath
198
 * @param tags - Object containing tags to be written
199
 * @param filebuffer - Can contain a filepath string or buffer
200
 * @param options - (optional) Object containing options
201
 * @param fn - (optional) Function for async version
202
 * @returns {boolean|Buffer|Error}
203
 */
204
module.exports.update = function(tags, filebuffer, options, fn) {
205
    if(!options || typeof options === 'function') {
206
        fn = fn || options
207
        options = {}
208
    }
209
210
    const rawTags = Object.keys(tags).reduce((acc, val) => {
211
        if(ID3Definitions.FRAME_IDENTIFIERS.v3[val] !== undefined) {
212
            acc[ID3Definitions.FRAME_IDENTIFIERS.v3[val]] = tags[val]
213
        } else {
214
            acc[val] = tags[val]
215
        }
216
        return acc
217
    }, {})
218
219
    const updateFn = (currentTags) => {
220
        currentTags = currentTags.raw || {}
221
        Object.keys(rawTags).map((specName) => {
222
            const options = ID3Util.getSpecOptions(specName, 3)
223
            const cCompare = {}
224
            if(options.multiple && currentTags[specName] && rawTags[specName]) {
225
                if(options.updateCompareKey) {
226
                    currentTags[specName].forEach((cTag, index) => {
227
                        cCompare[cTag[options.updateCompareKey]] = index
228
                    })
229
230
                }
231
                if (!(rawTags[specName] instanceof Array)) {
232
                    rawTags[specName] = [rawTags[specName]]
233
                }
234
                rawTags[specName].forEach((rTag) => {
235
                    const comparison = cCompare[rTag[options.updateCompareKey]]
236
                    if (comparison !== undefined) {
237
                        currentTags[specName][comparison] = rTag
238
                    } else {
239
                        currentTags[specName].push(rTag)
240
                    }
241
                })
242
            } else {
243
                currentTags[specName] = rawTags[specName]
244
            }
245
        })
246
247
        return currentTags
248
    }
249
250
    if(!isFunction(fn)) {
251
        return this.write(updateFn(this.read(filebuffer, options)), filebuffer)
252
    }
253
254
    this.write(updateFn(this.read(filebuffer, options)), filebuffer, fn)
255
}
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
256
257
module.exports.getTagsFromBuffer = function(filebuffer, options) {
258
    let framePosition = ID3Util.getFramePosition(filebuffer)
259
    if(framePosition === -1) {
260
        return this.getTagsFromFrames([], 3, options)
261
    }
262
    const frameSize = ID3Util.decodeSize(filebuffer.slice(framePosition + 6, framePosition + 10)) + 10
263
    let ID3Frame = Buffer.alloc(frameSize + 1)
264
    filebuffer.copy(ID3Frame, 0, framePosition)
265
    //ID3 version e.g. 3 if ID3v2.3.0
266
    let ID3Version = ID3Frame[3]
267
    const tagFlags = ID3Util.parseTagHeaderFlags(ID3Frame)
268
    let extendedHeaderOffset = 0
269
    if(tagFlags.extendedHeader) {
270
        if(ID3Version === 3) {
271
            extendedHeaderOffset = 4 + filebuffer.readUInt32BE(10)
272
        } else if(ID3Version === 4) {
273
            extendedHeaderOffset = ID3Util.decodeSize(filebuffer.slice(10, 14))
274
        }
275
    }
276
    let ID3FrameBody = Buffer.alloc(frameSize - 10 - extendedHeaderOffset)
277
    filebuffer.copy(ID3FrameBody, 0, framePosition + 10 + extendedHeaderOffset)
278
279
    let frames = this.getFramesFromID3Body(ID3FrameBody, ID3Version, options)
280
281
    return this.getTagsFromFrames(frames, ID3Version, options)
282
}
283
284
module.exports.getFramesFromID3Body = function(ID3FrameBody, ID3Version, options = {}) {
285
    let currentPosition = 0
286
    let frames = []
287
    if(!ID3FrameBody || !(ID3FrameBody instanceof Buffer)) {
288
        return frames
289
    }
290
291
    let identifierSize = 4
292
    let textframeHeaderSize = 10
293
    if(ID3Version === 2) {
294
        identifierSize = 3
295
        textframeHeaderSize = 6
296
    }
297
298
    while(currentPosition < ID3FrameBody.length && ID3FrameBody[currentPosition] !== 0x00) {
299
        let bodyFrameHeader = Buffer.alloc(textframeHeaderSize)
300
        ID3FrameBody.copy(bodyFrameHeader, 0, currentPosition)
301
302
        let decodeSize = false
303
        if(ID3Version === 4) {
304
            decodeSize = true
305
        }
306
        let bodyFrameSize = ID3Util.getFrameSize(bodyFrameHeader, decodeSize, ID3Version)
307
        if(bodyFrameSize + 10 > (ID3FrameBody.length - currentPosition)) {
308
            break
309
        }
310
        const specName = bodyFrameHeader.toString('utf8', 0, identifierSize)
311
        if(options.exclude instanceof Array && options.exclude.includes(specName) || options.include instanceof Array && !options.include.includes(specName)) {
312
            currentPosition += bodyFrameSize + textframeHeaderSize
313
            continue
314
        }
315
        const frameHeaderFlags = ID3Util.parseFrameHeaderFlags(bodyFrameHeader, ID3Version)
316
        let bodyFrameBuffer = Buffer.alloc(bodyFrameSize)
317
        ID3FrameBody.copy(bodyFrameBuffer, 0, currentPosition + textframeHeaderSize + (frameHeaderFlags.dataLengthIndicator ? 4 : 0))
318
        //  Size of sub frame + its header
319
        currentPosition += bodyFrameSize + textframeHeaderSize
320
        frames.push({
321
            name: specName,
322
            flags: frameHeaderFlags,
323
            body: frameHeaderFlags.unsynchronisation ? ID3Util.processUnsynchronisedBuffer(bodyFrameBuffer) : bodyFrameBuffer
324
        })
325
    }
326
327
    return frames
328
}
329
330
module.exports.getTagsFromFrames = function(frames, ID3Version, options = {}) {
331
    let tags = { }
332
    let raw = { }
333
334
    frames.forEach((frame) => {
335
        let specName
336
        let identifier
337
        if(ID3Version === 2) {
338
            specName = ID3Definitions.FRAME_IDENTIFIERS.v3[ID3Definitions.FRAME_INTERNAL_IDENTIFIERS.v2[frame.name]]
339
            identifier = ID3Definitions.FRAME_INTERNAL_IDENTIFIERS.v2[frame.name]
340
        } else if(ID3Version === 3 || ID3Version === 4) {
341
            /**
342
             * Due to their similarity, it's possible to mix v3 and v4 frames even if they don't exist in their corrosponding spec.
343
             * Programs like Mp3tag allow you to do so, so we should allow reading e.g. v4 frames from a v3 ID3 Tag
344
             */
345
            specName = frame.name
346
            identifier = ID3Definitions.FRAME_INTERNAL_IDENTIFIERS.v3[frame.name] || ID3Definitions.FRAME_INTERNAL_IDENTIFIERS.v4[frame.name]
347
        }
348
349
        if(!specName || !identifier || frame.flags.encryption) {
350
            return
351
        }
352
353
        if(frame.flags.compression) {
354
            if(frame.body.length < 5) {
355
                return
356
            }
357
            const inflatedSize = frame.body.readInt32BE()
358
            /*
359
            * ID3 spec defines that compression is stored in ZLIB format, but doesn't specify if header is present or not.
360
            * ZLIB has a 2-byte header.
361
            * 1. try if header + body decompression
362
            * 2. else try if header is not stored (assume that all content is deflated "body")
363
            * 3. else try if inflation works if the header is omitted (implementation dependent)
364
            * */
365
            try {
366
                frame.body = zlib.inflateSync(frame.body.slice(4))
367
            } catch (e) {
368
                try {
369
                    frame.body = zlib.inflateRawSync(frame.body.slice(4))
370
                } catch (e) {
371
                    try {
372
                        frame.body = zlib.inflateRawSync(frame.body.slice(6))
373
                    } catch (e) {
374
                        return
375
                    }
376
                }
377
            }
378
            if(frame.body.length !== inflatedSize) {
379
                return
380
            }
381
        }
382
383
        let decoded
384
        if(ID3Frames[specName]) {
385
            decoded = ID3Frames[specName].read(frame.body, ID3Version, this)
386
        } else if(specName.startsWith('T')) {
387
            decoded = ID3Frames.GENERIC_TEXT.read(frame.body, ID3Version)
388
        } else if(specName.startsWith('W')) {
389
            decoded = ID3Frames.GENERIC_URL.read(frame.body, ID3Version)
390
        }
391
392
        if(decoded) {
393
            if(ID3Util.getSpecOptions(specName, ID3Version).multiple) {
394
                if(!options.onlyRaw) {
395
                    if(!tags[identifier]) {
396
                        tags[identifier] = []
397
                    }
398
                    tags[identifier].push(decoded)
399
                }
400
                if(!options.noRaw) {
401
                    if(!raw[specName]) {
402
                        raw[specName] = []
403
                    }
404
                    raw[specName].push(decoded)
405
                }
406
            } else {
407
                if(!options.onlyRaw) {
408
                    tags[identifier] = decoded
409
                }
410
                if(!options.noRaw) {
411
                    raw[specName] = decoded
412
                }
413
            }
414
        }
415
    })
416
417
    if(options.onlyRaw) {
418
        return raw
419
    }
420
    if(options.noRaw) {
421
        return tags
422
    }
423
424
    tags.raw = raw
425
    return tags
426
}
427
428
/**
429
 * Checks and removes already written ID3-Frames from a buffer
430
 * @param data - Buffer
431
 * @returns {boolean|Buffer}
432
 */
433
module.exports.removeTagsFromBuffer = removeTagsFromBuffer
434
function removeTagsFromBuffer(data) {
435
    let framePosition = ID3Util.getFramePosition(data)
436
437
    if (framePosition === -1) {
438
        return data
439
    }
440
441
    let hSize = Buffer.from([data[framePosition + 6], data[framePosition + 7], data[framePosition + 8], data[framePosition + 9]])
442
443
    const isMsbSet = !!((hSize[0] | hSize[1] | hSize[2] | hSize[3]) & 0x80)
444
    if (isMsbSet) {
445
        //  Invalid tag size (msb not 0)
446
        return false
447
    }
448
449
    if (data.length >= framePosition + 10) {
450
        const size = ID3Util.decodeSize(data.slice(framePosition + 6, framePosition + 10))
451
        return Buffer.concat([data.slice(0, framePosition), data.slice(framePosition + size + 10)])
452
    }
453
454
    return data
455
}
456
457
/**
458
 * @param {string} filepath - Filepath to file
459
 * @returns {boolean|Error}
460
 */
461
function removeTagsSync(filepath) {
462
    let data
463
    try {
464
        data = fs.readFileSync(filepath)
465
    } catch(error) {
466
        return error
467
    }
468
469
    const newData = removeTagsFromBuffer(data)
470
    if(!newData) {
471
        return false
472
    }
473
474
    try {
475
        fs.writeFileSync(filepath, newData, 'binary')
476
    } catch(error) {
477
        return error
478
    }
479
480
    return true
481
}
482
483
/**
484
 * @param {string} filepath - Filepath to file
485
 * @param {(error: Error) => void} fn - Function for async usage
486
 * @returns {void}
487
 */
488
function removeTagsAsync(filepath, fn) {
489
    fs.readFile(filepath, (error, data) => {
490
        if(error) {
491
            fn(error)
492
        }
493
494
        const newData = removeTagsFromBuffer(data)
495
        if(!newData) {
496
            fn(error)
497
            return
498
        }
499
500
        fs.writeFile(filepath, newData, 'binary', (error) => {
501
            if(error) {
502
                fn(error)
503
            } else {
504
                fn(false)
505
            }
506
        })
507
    })
508
}
509
510
/**
511
 * Checks and removes already written ID3-Frames from a file
512
 * @param {string} filepath - Filepath to file
513
 * @param fn - (optional) Function for async usage
514
 * @returns {boolean|Error}
515
 */
516
module.exports.removeTags = function(filepath, fn) {
517
    if(isFunction(fn)) {
518
        return removeTagsAsync(filepath, fn)
519
    }
520
    return removeTagsSync(filepath)
521
}
522
523
function makePromise(fn) {
524
    return new Promise((resolve, reject) => {
525
        fn((error, result) => {
526
            if(error) {
527
                reject(error)
528
            } else {
529
                resolve(result)
530
            }
531
        })
532
    })
533
}
534
535
module.exports.Promise = {
536
    write: (tags, file) => makePromise(this.write.bind(this, tags, file)),
537
    update: (tags, file) => makePromise(this.update.bind(this, tags, file)),
538
    create: (tags) => {
539
        return new Promise((resolve) => {
540
            this.create(tags, (buffer) => {
541
                resolve(buffer)
542
            })
543
        })
544
    },
545
    read: (file, options) => makePromise(this.read.bind(this, file, options)),
546
    removeTags: (filepath) => makePromise(this.removeTags.bind(this, filepath))
547
}
548
549
module.exports.Constants = ID3Definitions.Constants
550